import dalex as dx
import pandas as pd
import numpy as np
import warnings
import pickle
import matplotlib.pyplot as plt
warnings.filterwarnings("ignore")
df = pd.read_csv("housing_preprocessed.csv")
df = df.rename(columns={"<1H OCEAN": "1H OCEAN"})
xgb = pickle.load(open("xgb.pickle", "rb"))
df.head()
| longitude | latitude | housing_median_age | total_bedrooms | population | households | median_income | 1H OCEAN | INLAND | NEAR BAY | NEAR OCEAN | rooms_per_household | median_house_value | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.211155 | 0.567481 | 0.784314 | 0.019711 | 0.008941 | 0.020395 | 0.539668 | 0.0 | 0.0 | 1.0 | 0.0 | 0.046610 | 452600.0 |
| 1 | 0.212151 | 0.565356 | 0.392157 | 0.171349 | 0.067210 | 0.186842 | 0.538027 | 0.0 | 0.0 | 1.0 | 0.0 | 0.040945 | 358500.0 |
| 2 | 0.210159 | 0.564293 | 1.000000 | 0.029179 | 0.013818 | 0.028783 | 0.466028 | 0.0 | 0.0 | 1.0 | 0.0 | 0.056513 | 352100.0 |
| 3 | 0.209163 | 0.564293 | 1.000000 | 0.036163 | 0.015555 | 0.035691 | 0.354699 | 0.0 | 0.0 | 1.0 | 0.0 | 0.037750 | 341300.0 |
| 4 | 0.209163 | 0.564293 | 1.000000 | 0.043148 | 0.015752 | 0.042270 | 0.230776 | 0.0 | 0.0 | 1.0 | 0.0 | 0.041277 | 342200.0 |
X = df.drop("median_house_value", axis=1)
y = df[["median_house_value"]]
explainer = dx.Explainer(xgb, X, y)
Preparation of a new explainer is initiated -> data : 19643 rows 12 cols -> target variable : Parameter 'y' was a pandas.DataFrame. Converted to a numpy.ndarray. -> target variable : 19643 values -> model_class : xgboost.sklearn.XGBRegressor (default) -> label : Not specified, model's class short name will be used. (default) -> predict function : <function yhat_default at 0x0000024FEF8C8D30> will be used (default) -> predict function : Accepts only pandas.DataFrame, numpy.ndarray causes problems. -> predicted values : min = 2.73e+04, mean = 1.92e+05, max = 4.59e+05 -> model type : regression will be used (default) -> residual function : difference between y and yhat (default) -> residuals : min = -2.45e+05, mean = -1.54e+02, max = 3.06e+05 -> model_info : package xgboost A new explainer has been created!
def select_and_plot(index):
observation = X.loc[[index], :]
# ta linijka musi być zakomentowana, aby wykresy były widoczne w pliku html
# print("XGBoost predicts: {price}".format(price=xgb.predict(observation)))
ceteris_paribus = explainer.predict_profile(observation)
return ceteris_paribus.plot()
select_and_plot(1993)
Calculating ceteris paribus: 100%|████████████████████████████████████████████████████| 12/12 [00:00<00:00, 286.48it/s]
select_and_plot(1234)
Calculating ceteris paribus: 100%|████████████████████████████████████████████████████| 12/12 [00:00<00:00, 308.52it/s]
select_and_plot(1002)
Calculating ceteris paribus: 100%|████████████████████████████████████████████████████| 12/12 [00:00<00:00, 279.82it/s]
Wykresy dla trzech różnych obserwacji są do siebie bardzo zbliżone, oznacza to, że w tym zbiorze danych interakcje w zasadzie nie występują. Największe różnice można zauważyć w zmiennych 'longitude' i 'latitude'. Dla przykładu, w obserwacji nr 1 możemy zaobserwować istotny drop w okolicach 0.2. Z kolei w obserwacjach nr 2 i 3 ten drop w zasadzie nie występuje. Wynika to prawdopodbnie z tego, że model znalazł jakieś klastry droższych nieruchomości w danym rejonie. Wobec tego, zmienne 'longitude' i 'latitude' muszą zmienić się jednocześnie, aby znacząco zmienić predykcję modelu.
Zastanawiający jest drop w predykowanej cenie w okolicach wartości 0.8 zmiennej 'median_income'. Wydaje się to nielogiczne, bo dlaczego osoby więcej zarabiające miałyby mieć tańsze domy. Anomalia ta prawdopodbnie wynika z małej ilości danych dla tak bogatych domostw i model w tych miejscach zaczyna 'świrować'.
Po przekopaniu się przez kilkadziesiąt rekordów, nie udało mi się znaleźć takich dwóch wierszy, dla których wyliczone dekompozycje diametralnie by się od siebie różniły (np. w jednej finalna wartość predykcji by rosła a w drugiej malała wraz ze wzrostem tej samej zmiennej).
Metoda Ceteris Paribus pozwala w czytelny sposób rozeznać się w zachowaniu modelu w zależności od każdej ze zmiennych. Wydaje się, że lepsze zastosowanie ma ona dla danych bez interakcji, można wtedy łatwiej uogólnić zachowanie modelu dla jednej obserwacji na cały zbiór. Dodatkowo, łatwość interpretacji modelu Ceteris Paribus poprawiłoby gdyby zmienne nie były przeskalowane metodą Min-Max.